/* Copyright (c) 2011, Damjan Jovanovic <damjan dot jov aaa aaatt t tt gmail>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the author nor the
 *       names of the contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.googlecode.raisinrefinery;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.LinkedList;

public class PatchBackporter {
	private static class Patch {
		String patch;
		String[] affectedFiles;
		
		Patch(String patch, String[] affectedFiles) {
			this.patch = patch;
			this.affectedFiles = affectedFiles;
		}
		
		boolean applyPatch(File sourceDir) throws IOException {
	        System.out.print("Applying patch " + patch + " ... ");
	        LinkedList<String> output = ProcessUtils.runAndCollectOutputFor(sourceDir, null,
	                "git", "show", patch);
	        File file = File.createTempFile("wine-patch", ".patch");
	        ProcessUtils.writeOutput(output, file);
	        try {
		        ProcessUtils.runAndCollectOutputFor(sourceDir, null,
		                "patch", "-p1", "--dry-run", "-i", file.getCanonicalPath());
		        // the previous one didn't throw, do the real one now:
		        ProcessUtils.runAndCollectOutputFor(sourceDir, null,
		                "patch", "-p1", "-i", file.getCanonicalPath());
		        System.out.println("SUCCEEDED");
		        return true;
	        } catch (IOException ioEx) {
	        	System.out.println("FAILED");
	        	return false;
	        }
		}
	}

    private static boolean applyResourcePatch(File sourceDir, String patchPath) throws IOException {
        File file = File.createTempFile("wine-patch", ".patch");
        InputStream is = null;
        OutputStream os = null;
        try {
        	System.out.print("Applying patch " + patchPath + " manually ... ");
            is = PatchBackporter.class.getResourceAsStream(patchPath);
            os = new BufferedOutputStream(new FileOutputStream(file));
            byte[] buffer = new byte[8096];
            int bytesRead;
            while ((bytesRead = is.read(buffer)) >= 0) {
                os.write(buffer, 0, bytesRead);
            }
            os.flush();
            ProcessUtils.runAndCollectOutputFor(sourceDir, null,
                    "patch", "-p1", "--dry-run", "-i", file.getCanonicalPath());
            // the previous one didn't throw, do the real one now:
            ProcessUtils.runAndCollectOutputFor(sourceDir, null,
                    "patch", "-p1", "-i", file.getCanonicalPath());
            System.out.println("SUCCESS");
            return true;
        } catch (IOException ioEx) {
        	System.out.println("FAILURE");
        	return false;
        } finally {
            Main.safelyClose(is);
            Main.safelyClose(os);
        }
    }
	
	private static void searchAndReplace(File file, String search, String replace) throws IOException {
		BufferedReader r = null;
		ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
		try {
			r = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
			String line;
			while ((line = r.readLine()) != null) {
				byte[] bytes = line.replace(search, replace).getBytes();
				baos.write(bytes);
				baos.write("\n".getBytes());
			}
		} finally {
			Main.safelyClose(r);
		}
		
		baos.flush();
		byte[] contents = baos.toByteArray();
		
		BufferedOutputStream w = null;
		try {
			w = new BufferedOutputStream(new FileOutputStream(file));
			w.write(contents);
		} finally {
			Main.safelyClose(w);
		}
	}

    private static Patch[] patches = {
    	// Define obsoleted CLK_TCK to CLOCKS_PER_SEC if not there.
    	new Patch("5c256c518faf2344266cabecad9fdbfd9d0095ad",
    			new String[] {"dlls/kernel/time.c"}),
    			
    	// Avoid getting ahead of dsound mixer when using non-hw device. Also
    	// avoid using internal ALSA call to retrieve hardware play position.
    	new Patch("828086ef930279d1166417401f28e2ebcce2f53a",
    			new String[] {"dlls/winmm/winealsa/audio.c"}),
    	
    	// Needed for the next patch:
    	// Make WineEngGetFontData always return the used byte count.
    	new Patch("c7e860d48a83fe8825681fc3b925d6f2551f1453",
    			new String[] {"dlls/gdi/freetype.c"}),

    	// Needed for next patch:
    	// gdi: Use correct include file for FT_Get_Module.
        new Patch("603d21cbc4fc8b915248cd3a6f90b02f721980c4",
        		new String[] {"configure", "configure.ac", "dlls/gdi/freetype.c", "include/config.h.in"}),
        		
    	// gdi: Fixes to preempt freetype 2.2.0.
    	new Patch("0458a5e38dcf7d03a0eedd1e63922c1eb4e37cb9",
    			new String[] {"dlls/gdi/freetype.c"}),
    	
    	// gdi32: Adjust FT_MulFix function to Freetype cvs head.
        new Patch("de251e1c62651d8160b340a4fb9d0f9834a8e538",
        		new String[] {"dlls/gdi32/freetype.c", "dlls/gdi/freetype.c"}) {
        	@Override
        	boolean applyPatch(File sourceDir) throws IOException {
        		if (super.applyPatch(sourceDir)) {
        			return true;
        		}
        		if (applyResourcePatch(sourceDir,
        				"/patches/de251e1c62651d8160b340a4fb9d0f9834a8e538-wine-0.9.20.patch")) {
        			return true;
        		}
        		System.out.println("Patch " + patch + " is having really bad luck!");
        		return false;
        	}
        },
        
        // ntdll: Add local defines for VIF and VIP flags.
        new Patch("3499cb96b69d0c99da5ae68f5f7052c2ce8e66be",
        		new String[] {"dlls/ntdll/signal_i386.c"}) {
        	@Override
        	boolean applyPatch(File sourceDir) throws IOException {
        		if (!super.applyPatch(sourceDir)) {
        			try {
        				searchAndReplace(new File(sourceDir + "/dlls/ntdll/signal_i386.c"), "VIF_MASK", "VIF_FLAG");
        				searchAndReplace(new File(sourceDir + "/dlls/ntdll/signal_i386.c"), "VIP_MASK", "VIP_FLAG");
        			} catch (IOException ioEx) {
        				ioEx.printStackTrace();
        				System.out.println("Patch " + patch + " is having really bad luck!");
        				return false;
        			}
        			if (!applyResourcePatch(sourceDir, "/patches/" + patch + ".patch")) {
        				System.out.println("Patch " + patch + " is having really bad luck!");
        				return false;
        			}
        		}
        		return true;
        	}
        },
        
        // wininet: Include openssl/ssl.h before Windows headers.
        new Patch("7c616799894faa48ee3277bc276a39bed55780e0",
        		new String[] {"dlls/wininet/internet.h", "dlls/wininet/netconnection.c"}) {
        	@Override
        	boolean applyPatch(File sourceDir) throws IOException {
        		if (!super.applyPatch(sourceDir)) {
        			try {
	        			searchAndReplace(new File(sourceDir + "/dlls/wininet/netconnection.c"),
	        					"#include \"wine/port.h\"",
	        					"#include \"wine/port.h\"\n#include <sys/types.h>");
        			} catch (IOException ioEx) {
        				System.out.println("Patch " + patch + " is having really bad luck!");
        				ioEx.printStackTrace();
        				return false;
        			}
        			if (!applyResourcePatch(sourceDir, "/patches/" + patch + ".patch")) {
        				System.out.println("Patch " + patch + " is having really bad luck!");
        				return false;
        			}
        		}
        		return true;
        	}
        },
        
//        // Makedll.rules: Add a generic EXTRADLLFLAGS variable instead of defining a specific variable for every possible parameter.
//        new Patch("a5fb981acea5ce23d134ea5fbb5f79d274783a19",
//        	    new String[] {"dlls/Makedll.rules.in", "dlls/kernel32/Makefile.in", "dlls/ntdll/Makefile.in"} ) {
//        	@Override
//        	boolean applyPatch(File sourceDir) throws IOException {
//        		if (super.applyPatch(sourceDir)) {
//        			return true;
//        		}
//        		if (applyResourcePatch(sourceDir,
//        				"/patches/a5fb981acea5ce23d134ea5fbb5f79d274783a19-wine-0.9.30.patch")) {
//        			return true;
//        		}
//        		if (applyResourcePatch(sourceDir,
//        				"/patches/a5fb981acea5ce23d134ea5fbb5f79d274783a19-wine-0.9.20.patch")) {
//        			return true;
//        		}
//        		if (applyResourcePatch(sourceDir,
//        				"/patches/a5fb981acea5ce23d134ea5fbb5f79d274783a19-wine-0.9.17ish-2.patch")) {
//        			return true;
//        		}
//        		if (applyResourcePatch(sourceDir,
//        				"/patches/a5fb981acea5ce23d134ea5fbb5f79d274783a19-wine-0.9.17ish.patch")) {
//        			return true;
//        		}
//        		if (applyResourcePatch(sourceDir,
//        				"/patches/a5fb981acea5ce23d134ea5fbb5f79d274783a19-wine-0.9.15ish.patch")) {
//        			return true;
//        		}
//        		if (applyResourcePatch(sourceDir,
//        				"/patches/a5fb981acea5ce23d134ea5fbb5f79d274783a19-wine-0.9.10.patch")) {
//        			return true;
//        		}
//        		System.out.println("Patch " + patch + " is having really bad luck!");
//        		return false;
//        	}
//        },
//        	
//        // winebuild: Don't set the IMAGE_FILE_DLL flag on native modules.
//        new Patch("b462c20876feae7b1384a50a8acfa5b376562bda",
//        		new String[] {"tools/winebuild/main.c"} ),
        		
        // wldap32: Fix build against latest OpenLDAP release.
        new Patch("032dbfe47493648084f57e24d019d63bfbe04dca",
        		new String[] { "configure", "configure.ac", "dlls/wldap32/parse.c",
        			"dlls/wldap32/winldap_private.h", "include/config.h.in"}) {
        	@Override
        	boolean applyPatch(File sourceDir) throws IOException {
        		if (super.applyPatch(sourceDir)) {
        			return true;
        		}
        		if (applyResourcePatch(sourceDir,
        				"/patches/032dbfe47493648084f57e24d019d63bfbe04dca-wine-0.9.20.patch")) {
        			return true;
        		}
        		System.out.println("Patch " + patch + " is having really bad luck!");
        		return false;
        	}
        },
        			
        // mscms: Fix compilation with the latest lcms header.
        new Patch("83f1c1df254c8613daea2bec5645887b6b13a18c",
        		new String[] {"dlls/mscms/mscms_priv.h"}),

        // Needed for next patch:
        // Preserve 16-byte stack alignment in the various assembly
        // functions. Needed for MacOSX.
        new Patch("cc01e8ff05d2461d7bd9c14281057254429518c0",
        		new String[] {}) { // too many files
        	@Override
        	boolean applyPatch(File sourceDir) throws IOException {
        		if (super.applyPatch(sourceDir)) {
        			return true;
        		}
        		if (applyResourcePatch(sourceDir, "/patches/cc01e8ff05d2461d7bd9c14281057254429518c0-0.9.patch"))  {
        			return true;
        		}
        		System.out.println("Patch " + patch + " is having really bad luck!");
        		return false;
        	}
        },

        // preloader: Setup a fake thread-local storage block pointed to by %gs.
        new Patch("a68d293bae6cec6deafafb94972c9277791256b5",
        		new String[] {"libs/wine/ldt.c", "loader/preloader.c"}),
        
//        // winegcc: Automatically add the correct lib paths when building inside the Wine tree.
//        new Patch("523e46debbe004ff8023d47d5b514811a7c76679",
//        		new String[] {"Make.rules.in", "dlls/Makedll.rules.in",
//        		"dlls/Maketest.rules.in", "programs/Makeprog.rules.in",
//        		"tools/winegcc/winegcc.c"
//        }),
        	
        // wrc: Don't use identifiers starting yy*, they're reserved for flex.
        new Patch("69dcbaaf93a65820a28fe2485b6d15a90f423899",
        		new String[] {"tools/wrc/parser.l"}),
        		
        // preloader: Allow compiling the preloader with -fstack-protector.
        new Patch("7cd9a8f8dc9536393e4e6042ae18bfb89b8671e5",
        		new String[] {"loader/preloader.c"}),
        
        // d3d8: Header cleanup.
        // Remove unneeded structures and functions from d3d8_private.h, clean
        // some datatypes up and remove d3dcore_gl.h.
        new Patch("a9665eda799e391fcb2bfd72eba83dbc8098cedb",
        		new String[] {"dlls/d3d8/d3d8_private.h", "dlls/d3d8/d3dcore_gl.h"}) {
        	@Override
        	boolean applyPatch(File sourceDir) throws IOException {
        		if (applyResourcePatch(sourceDir,
        				"/patches/a9665eda799e391fcb2bfd72eba83dbc8098cedb-wine-0.9.patch")) {
        			return true;
        		}
        		System.out.println("Patch " + patch + " is having really bad luck!");
        		return false;
        	}
        },
        		
    };
    
    private static final String[] revisionsThatDontCompile = {
        // msxml3: Added ISAXMLReader_getContentHandler implementation.
        // saxreader.c: In function ‘isaxxmlreader_getContentHandler’:
        // saxreader.c:509:12: error: ‘saxreader’ has no member named ‘contentHandler’
        // saxreader.c:510:9: error: ‘saxreader’ has no member named ‘contentHandler’
        // saxreader.c:510:9: error: ‘saxreader’ has no member named ‘contentHandler’
        // saxreader.c:511:28: error: ‘saxreader’ has no member named ‘contentHandler’
        "5b350582b92d2d74a9638a8e516bfde78f660ea5",

        // make[2]: *** No rule to make target `shell32_Cn.rc', needed by `shres.res'.  Stop.
        "2bf33c0a324d98406a05e9d3721cb4faa51692c2",

        // make[2]: *** No rule to make target `comctl_Cn.rc', needed by `rsrc.res'.  Stop.
        "c5ef467f4dd6d1d225294289f78113c5fd1eed7d",
        
        // dispex.c:69:6: error: ‘DIID_DispHTMLBody’ undeclared here (not in a function)
        // dispex.c:85:6: error: ‘IID_IHTMLControlElement’ undeclared here (not in a function)
        // dispex.c:96:6: error: ‘IID_IHTMLElement3’ undeclared here (not in a function)
        // dispex.c:97:6: error: ‘IID_IHTMLElement4’ undeclared here (not in a function)
        // dispex.c:106:6: error: ‘IID_IHTMLUniqueName’ undeclared here (not in a function)
        "cf86ff8f7e63a810876040e4d8a5806d472b0b3f",

        // dispex.c:69:6: error: ‘DIID_DispHTMLBody’ undeclared here (not in a function)
        "e2c3be83fdc8559999f239f705aa851d410129e1",

        // /usr/bin/install: cannot create regular file `/wine-out/bin/programs/msiexec': No such file or directory
        "9463687f78977058e6f114fdbd56f8a2bd754a47",

        // ditto
        "c6e62dda1c1d71ebcaf5a6fc347f0e26ef43bebd",

        // ditto
        "3be8b409ae360e87fed6795616a6c9904da17471",

        // ditto
        "52c400e97e031b8c7d0556e83119c67c2576e314",

        // ditto
        "93f03393875d241d339289ac559921684b76efc6",

        // ditto
        "1431e24feeb20dac8a2e791f63cb0a7d4c32c1bf",

        // ditto
        "6e116f521585331876f02df3ed8abdbc363cb3b0"
    };

    static {
        Arrays.sort(revisionsThatDontCompile);
    }

    public static boolean canCompileRevision(String revision) {
        return Arrays.binarySearch(revisionsThatDontCompile, revision) < 0;
    }

    public static void backportPatches(File sourceDir, String revision) throws IOException {
        for (Patch patch : patches) {
            if (revisionPreceedsPatch(sourceDir, revision, patch.patch)) {
            	patch.applyPatch(sourceDir);
            }
        }
    }
    
    private static boolean revisionPreceedsPatch(File sourceDir,
            String revision, String patch) throws IOException {

        if (patch.equals(revision)) {
            return false;
        }
        // FIXME: is there a better/faster way to query
        // whether one commit preceeds another?
        String revisions = revision + ".." + patch;
        LinkedList<String> output = ProcessUtils.runAndCollectOutputFor(
                sourceDir, null,
                "git", "rev-list", "--max-count=1", revisions);
        return output.size() > 0;
    }
}
